package com.butterfly.encrypt.util;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.apache.log4j.Logger;
import org.jdom.Document;

/**
 * 私钥加签，公钥解签的工具类
 * @author fox email:hitman__47@163.com
 * @version 1.0 2013-10-28
 *
 */
public class Sign {
	private static final  Logger log = Logger.getLogger(Sign.class);
	/**
	 * 将指定内容用相应目录下的私钥文件进行加密，并返回加密后的结果(加签)
	 * @param plain
	 * 				需要加密的xml对象转换成的字节流
	 * @param prikey
	 * 				私钥
	 * @return
	 * 			加密后的结果，如果没有找到私钥以及发生异常则返回null
	 */
	public static byte[] sign(byte[] plain,RSAPrivateKey prikey){
		
		if(prikey==null){
			log.info("加签用的私钥为空 !");
			return null;
		}
		/**
		 * 使用 Signature 对象签名数据或验证签名包括以下三个阶段：
		 * 1 初始化，使用 
		 * 				初始化验证签名的公钥 （请参见 initVerify）
		 * 				初始化签署签名的私钥   请参见 initSign(PrivateKey) 和 initSign(PrivateKey, SecureRandom)
		 * 2更新
		 * 			根据初始化类型，这可更新要签名或验证的字节。请参见 update 方法.
		 * 3签署或验证所有更新字节的签名。请参见 sign 方法和 verify 方法。 
		 * 注意，由于历史原因，此类是从 SignatureSpi 中抽象出来的，并进行了扩展。应用程序开发人员只需注意在此 Signature 类中定义的方法即可；超类中的所有方法都是供那些希望提供自己的数字签名算法实现的加密服务提供程序使用的。
		 */
		try {
			Signature sign=Signature.getInstance("SHA1withRSA");
			//使用来自给定证书的公钥初始化此用于验证的对象。 
			sign.initSign(prikey);
			//更新要由字节签名验证的数据。  参数b - 用于更新的字节。 即 明文。
			sign.update(plain);
			/**
			 * 返回所有已更新数据的签名字节。签名的格式取决于基础签名方案。 
			 * 对此方法的调用将把此签名对象重新设置到以前为进行签名而通过调用 initSign(PrivateKey) 对其初始化时的状态。
			 * 也就是说，如果需要，通过重新调用 update 和 sign，可重新设置对象，并通过它从同一个签署者中生成其他人的签名。
			 * 返回：
			 *  	签名操作结果的签名字节。 
			 *  抛出：
			 *  	 SignatureException - 如果此签名对象未得到正确初始化，或者此签名算法不能处理所提供的输入数据。
			 */
			log.info("加签成功!");
			return(sign.sign());
		} catch (NoSuchAlgorithmException e) {
			log.error(e.getMessage(),e);
		}catch (InvalidKeyException e) {
			log.error(e.getMessage(),e);
		}catch (SignatureException e) {
			log.error(e.getMessage(),e);
		}
		return null;
	}
	/**
	 *  用相应目录下的公钥文件校验用私钥文件加密的内容，如果校验通过，返回true;如果校验不通过，返回false（验签）
	 * @param plain
	 * 				 需要加密的xml对象转换成字节流（明文+签名）
	 * @param document
	 * 				需要加密的xml对象(明文+签名)的Document对象
	 * @param sign
	 * 				xml对象中的SIGN字段，将xml用私钥文件加密生成的签名串
	 * @param pubKey
	 * 				 公钥
	 * @return
	 * 			验签结果，如果校验通过，返回true，如果校验不通过，返回false
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeyException 
	 * @throws SignatureException 
	 * @throws IOException 
	 */
	public static boolean verifySign(byte[] plain,Document document,String sign,RSAPublicKey pubKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException{
		/**
		 * 使用 Signature 对象签名数据或验证签名包括以下三个阶段：
		 * 1 初始化，使用 
		 * 				初始化验证签名的公钥 （请参见 initVerify）
		 * 				初始化签署签名的私钥   请参见 initSign(PrivateKey) 和 initSign(PrivateKey, SecureRandom)
		 * 2更新
		 * 			根据初始化类型，这可更新要签名或验证的字节。请参见 update 方法.
		 * 3签署或验证所有更新字节的签名。请参见 sign 方法和 verify 方法。 
		 * 注意，由于历史原因，此类是从 SignatureSpi 中抽象出来的，并进行了扩展。应用程序开发人员只需注意在此 Signature 类中定义的方法即可；超类中的所有方法都是供那些希望提供自己的数字签名算法实现的加密服务提供程序使用的。
		 */
		Signature verify=Signature.getInstance("SHA1withRSA");
		//使用来自给定证书的公钥初始化此用于验证的对象。 
		verify.initVerify(pubKey);
		//更新要由字节签名验证的数据。  参数b - 用于更新的字节。 即 明文。
		verify.update(Tools.toByteArrays(document));
		//得到base64解码后的 签名
		byte[] signByte=Base64.decodeByteByString(sign);
		/**
		 * 验证传入的签名。 
		 * 	参数：
		 * 		signature - 要验证的签名字节。
		 *  返回：
		 *		  如果签名得到验证，则返回 true，否则将返回 false。 
		 *  抛出： 
		 *        SignatureException - 如果此签名对象未得到正确初始化，传入的签名未得到正确解码，或类型错误，如果此签名算法不能处理所提供的输入数据等。
		 */
		boolean flag=verify.verify(signByte);
		
		if(!flag){
			String xml=new String(plain,"UTF-8").trim();
			String signSrc=xml.replaceAll("(<SIGN>[\\S\\s]*</SIGN>)", "");
			verify.update(signSrc.getBytes());
			flag=verify.verify(signByte);
		}
		if(flag==true)
			log.info("验签成功！");
		else
			log.info("验签失败！");
		
		return flag;
		
	}
	/**
	 *  返回指定路径下的私钥对象
	 * @param priKeyPath
	 * 					 私钥文件存放的目录
	 * @return
	 * 			私钥对象
	 */
	public static RSAPrivateKey getPriKey(String priKeyPath){
		try {
			return readPKCS8PrivateKey(priKeyPath);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
			return null;
		}
		
	}
	/**
	 * 读取指定路径下的私钥对象
	 * @param priKeyPath
	 * 					 私钥文件存放目录
	 * @return
	 * 			私钥对象
	 * @throws IOException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeySpecException 
	 */
	private static RSAPrivateKey readPKCS8PrivateKey(String path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
		FileInputStream private_file_in= new FileInputStream(path);
		log.info("可以不受阻塞地从此文件输入流中读取的字节数="+private_file_in.available());
		byte pri_key_bs[]= new byte[private_file_in.available()];
		int num=private_file_in.read(pri_key_bs);
		log.info("readIn="+num);
		//PKCS8EncodedKeySpec表示按照 ASN.1 类型 PrivateKeyInfo 进行编码的专用密钥的 ASN.1 编码。
		/**
		 * 根据给定的编码密钥创建一个新的 PKCS8EncodedKeySpec。 
		 * 参数：
		 * 		encodedKey - 假设按照 PKCS #8 标准对其进行编码的密钥。复制数组的内容，以防随后的修改。
		 */
		PKCS8EncodedKeySpec spec= new PKCS8EncodedKeySpec(pri_key_bs);
		/**
		 * 生成实现指定算法的 KeyFactory 对象。如果默认的 provider 包提供所请求算法的实现，则返回包含该实现的 KeyFactory 的实例。如果默认包中不存在该算法，则搜索其他包。 

		 * 参数：
		 * 		algorithm - 所请求密钥算法的名称。有关标准算法名称的信息，请参阅《Java Cryptography Architecture API Specification & Reference》中的附录 A。 
		 * 返回：
		 * 		指定算法的 KeyFactory 对象。 
		 * 抛出： 
		 * 		NoSuchAlgorithmException - 如果默认的提供程序包中请求的算法不可用，或者搜索到的其他提供程序包都不可用。
		 */
		KeyFactory kfac= KeyFactory.getInstance("RSA");
		private_file_in.close();
		//根据所提供的密钥规范（密钥材料）生成私钥对象。 
		//参数：keySpec - 私钥的规范（密钥材料）。 
		//返回：私钥。
		RSAPrivateKey private_key=(RSAPrivateKey)kfac.generatePrivate(spec);
		return private_key;
	}
	/**
	 * 返回指定路径下的公钥对象
	 * @param pubKeyPath
	 * 					公钥文件存放目录
	 * @return
	 * 			公钥对象
	 */
	public static RSAPublicKey getPubKey(String pubKeyPath){
		
		try {
			return readX509PublicKey(pubKeyPath);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
			return null;
		}
	}
	/**
	 * 读取指定路径下的公钥对象
	 * @param path
	 * 				公钥文件存放目录
	 * @return
	 * 			公钥对象
	 * @throws IOException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeySpecException 
	 */
	private static RSAPublicKey readX509PublicKey(String path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
		FileInputStream public_file_in=new FileInputStream(path);
		log.info("可以不受阻塞地从此文件输入流中读取的字节数="+public_file_in.available());
		byte pub_key_bs[] = new byte[public_file_in.available()];
		public_file_in.read(pub_key_bs);
		//X509EncodedKeySpec表示根据 ASN.1 类型 SubjectPublicKeyInfo 进行编码的公用密钥的 ASN.1 编码。
		X509EncodedKeySpec spec = new X509EncodedKeySpec(pub_key_bs);
		/**
		 * 生成实现指定算法的 KeyFactory 对象。如果默认的 provider 包提供所请求算法的实现，则返回包含该实现的 KeyFactory 的实例。如果默认包中不存在该算法，则搜索其他包。 

		 * 参数：
		 * 		algorithm - 所请求密钥算法的名称。有关标准算法名称的信息，请参阅《Java Cryptography Architecture API Specification & Reference》中的附录 A。 
		 * 返回：
		 * 		指定算法的 KeyFactory 对象。 
		 * 抛出： 
		 * 		NoSuchAlgorithmException - 如果默认的提供程序包中请求的算法不可用，或者搜索到的其他提供程序包都不可用。
		 */
		KeyFactory kfac= KeyFactory.getInstance("RSA");
		//根据所提供的密钥规范（密钥材料）生成公钥对象。 
		RSAPublicKey public_key = (RSAPublicKey) kfac.generatePublic(spec);
		public_file_in.close();
		return public_key;
	}
}
